function [decisions, dvals] = slclassify(scores, rlabels, sch, varargin)
%SLCLASSIFY Classifies a set of samples based on score matrix
%
% [ Syntax ]
%   - decisions = slclassify(scores, clabels, sch, ...)
%   - [decisions, dvals] = slclassify(scores, clabels, sch, ...)
%
% [ Arguments ]
%   - scores:       the score matrix (nr x nq)
%   - rlabels:      the class labels of reference samples
%                   If rlabels is empty, it is set to 1 : nr.
%   - sch:          the scheme to use
%   - decisions:    the classification decisions (1 x nq)
%   - dvals:        the values based on which the decisions are made
%
% [ Description ]
%   - decisions = slclassify(scores, rlabels, sch, ...) classifies the 
%     query samples according their comparison scores with reference
%     samples. 
%
%     Suppose there are nq query samples and nr reference samples. Then
%     scores should be a nr x nq matrix, with scores(i, j) being the
%     comparison score between the j-th query and the i-th reference.
%     rlabels should be a vector of length nr giving the class labels of
%     the reference samples. If the reference labels are in natural order
%     as 1 : nr, then we can simply input rlabels as an empty array.
%
%     A series of schemes are available for classification:
%     \{:
%         - highest:      classify a query sample to the class of the
%                         reference sample with highest score.
%         - lowest:       classify a query sample to the class of the
%                         reference sample with lowest score.
%         - high-maxn:    Find a set of neighboring references (with 
%                         highest scores) for each query, and classify the 
%                         sample to the class containing maxinum number of 
%                         neighbor query.
%         - low-maxn:     Find a set of neighboring references (with 
%                         lowest scores) for each query, and classify the 
%                         sample to the class containing maxinum number of 
%                         neighbor query.
%         - high-sum:     Find a set of neighboring references (with 
%                         highest scores) for each query, and classify 
%                         the sample to the class in which the sum of 
%                         neighboring reference scores is largest.
%         - high-avg:     Find a set of neighboring references (with 
%                         highest scores) for each query, and classify 
%                         the sample to the class in which the mean of 
%                         neighboring reference scores is highest.
%         - low-avg:      Find a set of neighboring references (with 
%                         highest scores) for each query, and classify 
%                         the sample to the class in which the sum of 
%                         neighboring reference scores is largest.
%     \:}
%
%     You can further specify the following options
%     \{:
%         - K:              The maximum neighborhood size. 
%
%                           By default it is set to zero. It takes effects
%                           for all schemes except 'highest' and 'lowest'.
%
%         - exclude_self:   By setting this option to be true when the
%                           reference and query are the same, it can 
%                           exclude the query sample from the references
%                           for each query. (default = false)
%
%         - score_thres:    The score threshold for candidate references.
%                           
%                             - For the schemes 'highest', the decision for
%                               a query will be set to NaN, when the highest
%                               score for the query is below the score_thres.
%
%                             - For the schemes 'lowest', the decision for a 
%                               query will be set to NaN, when the lowest
%                               score for the query is larger than the
%                               score_thres.
%
%                             - For the schemes 'high-maxn', 'high-sum', and
%                               'high-avg', only the references with scores
%                               higher than the score_thres can be put in the
%                               neighbor set. The decision for a query with an
%                               empty neighbor set is set to NaN.
%
%                             - For the schemes 'low-maxn', and 'low-avg', only
%                               the references with scores lower than the
%                               score_thres can be put in neighbor set. The
%                               decision for a query with an empty neighbor set
%                               is set to NaN.
%
%                           By default, it it set to [], which means no
%                           threshold is set for the scores.
%     \:}
%
%   - [decisions, dvals] = slclassify(scores, clabels, sch, ...)
%     additionally returns the values based on which the decisions are
%     made.
%
%       - For the schemes 'highest' and 'lowest', the values are the
%         corresponding highest/lowest score values.
%
%       - For the schemes 'high-maxn' and 'low-maxn', the values are the
%         corresponding maximum number of neighboring references.
%
%       - For the schemes 'high-sum', the values are the corresponding sum
%         of neighboring reference score values.
%
%       - For the schemes 'high-avg' and 'low-avg', the values are the 
%         corresponding average of neighboring reference scores.
%
%     The values corresponding to NaN decisions are also NaN.
%       
% [ History ]
%   - Created by Dahua Lin, on Aug 9th, 2006
%   - Modified by Dahua Lin, on Aug 16th, 2006
%       - eliminate the qlabel parameters, which are essentially not
%         needed.
%       - add functionality to support schemes. In current revision,
%         it supports nearest-neighbor ('nn') and leave-one-out ('loo').
%   - Rewritten by Dahua Lin, on Jul 18, 2007
%       - Support different classification schemes
%       - Support empty rlabels for natural labeling
%       - Support score-thresholding and NaN decision
%

%% parse and verify input arguments

error(nargchk(3, inf, nargin));

assert(isnumeric(scores) && ndims(scores) == 2 && isreal(scores), ...
    'sltoolbox:slclassify:invalidarg', 'The scores should be a 2D real matrix.');
nr = size(scores, 1);

if ~isempty(rlabels)
    assert(isnumeric(rlabels) && isvector(rlabels), ...
        'sltoolbox:slclassify:invalidarg', 'The rlabels should be a numeric vector.');
else
    rlabels = 1 : nr;
end
    
assert(nr == length(rlabels), 'sltoolbox:slclassify:invalidarg', ...
    'The number of references in scores does not match the length of rlabels.');

assert(ischar(sch), 'sltoolbox:slclassify:invalidarg', ...
    'The sch should be a string giving the name of the classification scheme.');

opts = struct( ...
    'K', 0, ...
    'exclude_self', false, ...
    'score_thres', []);

if~isempty(varargin)
    opts = setopts(opts, varargin{:});
end

assert(islogical(opts.exclude_self) && isscalar(opts.exclude_self), ...
    'sltoolbox:slclassify:invalidopt', 'The exclude_self option should be a logical scalar.');
assert(isempty(opts.score_thres) || (isnumeric(opts.score_thres) && isscalar(opts.score_thres)), ...
    'sltoolbox:slclassify:invalidopt', 'The score_thres option should be a numeric scalar or empty.');

if ~strcmp(sch, 'highest') && ~strcmp(sch, 'lowest')
    assert(isnumeric(opts.K) && isscalar(opts.K) && opts.K > 0 && opts.K == fix(opts.K), ...
        'sltoolbox:slclassify:invalidopt', 'The option K should be set to a positive integer scalar.');
end


%% main 

switch sch
    case 'highest'
        f = @classify_highest;
        is_sim = true;
        use_nb = false;
        
    case 'lowest'
        f = @classify_lowest;
        is_sim = false;
        use_nb = false;
        
    case 'high-maxn'
        f = @classify_highmaxn;
        is_sim = true;
        use_nb = true;
        
    case 'low-maxn'
        f = @classify_lowmaxn;
        is_sim = false;
        use_nb = true;
        
    case 'high-sum'
        f = @classify_highsum;
        is_sim = true;
        use_nb = true;
        
    case 'high-avg'
        f = @classify_highavg;
        is_sim = true;
        use_nb = true;
        
    case 'low-avg'
        f = @classify_lowavg;
        is_sim = false;
        use_nb = true;
        
    otherwise
        error('sltoolbox:slclassify:unknownsch', ...
            'Unknown classification scheme %s', sch);
end

if size(rlabels, 1) > 1
    rlabels = reshape(rlabels, [1 nr]);
end

if opts.exclude_self
    assert(size(scores, 2) == nr, 'sltoolbox:slclassify:invalidarg', ...
        'scores should be a square matrix when exclude_self is true.');
    
   if is_sim
       scores(1:(nr+1):(nr*nr)) = -inf;
   else
       scores(1:(nr+1):(nr*nr)) = inf;
   end
end
   
if use_nb
    [decisions, dvals] = f(scores, rlabels, opts.K, opts.score_thres);
else
    [decisions, dvals] = f(scores, rlabels, opts.score_thres);
end


%% classification schemes

function [dec, dvals] = classify_highest(scores, rlabels, sthres)

[dvals, si] = max(scores, [], 1);
dec = rlabels(si);

if ~isempty(sthres)
    not_valid = dvals < sthres;
    dvals(not_valid) = nan;
    dec(not_valid) = nan;
end


function [dec, dvals] = classify_lowest(scores, rlabels, sthres)

[dvals, si] = min(scores, [], 1);
dec = rlabels(si);

if ~isempty(sthres)
    not_valid = dvals > sthres;
    dvals(not_valid) = nan;
    dec(not_valid) = nan;
end


function [dec, dvals] = classify_highmaxn(scores, rlabels, K, sthres)

fil = get_filter(sthres, true);
[dec, dvals] = decide_from_labelednbs(scores, rlabels, K, fil, @numel, true, true);


function [dec, dvals] = classify_lowmaxn(scores, rlabels, K, sthres)

fil = get_filter(sthres, false);
[dec, dvals] = decide_from_labelednbs(scores, rlabels, K, fil, @numel, false, true);


function [dec, dvals] = classify_highsum(scores, rlabels, K, sthres)

fil = get_filter(sthres, true);
[dec, dvals] = decide_from_labelednbs(scores, rlabels, K, fil, @sum, true, true);


function [dec, dvals] = classify_highavg(scores, rlabels, K, sthres)

fil = get_filter(sthres, true);
[dec, dvals] = decide_from_labelednbs(scores, rlabels, K, fil, @(v) sum(v)/numel(v), true, true);


function [dec, dvals] = classify_lowavg(scores, rlabels, K, sthres)

fil = get_filter(sthres, false);
[dec, dvals] = decide_from_labelednbs(scores, rlabels, K, fil, @(v) sum(v)/numel(v), false, false);


%% auxiliary function

function fil = get_filter(sthres, is_sim)

if isempty(sthres)
    fil = [];
else
    if is_sim
        fil = @(x) x >= sthres;
    else
        fil = @(x) x <= sthres;
    end
end



